package gel;

import static robocode.util.Utils.normalRelativeAngleDegrees;
import robocode.HitByBulletEvent;
import robocode.HitRobotEvent;
import robocode.Robot;
import robocode.ScannedRobotEvent;
import robocode.util.Utils;

/**
 * First competitive robot designed to take out the sample Robocode robots. Borrows some code from
 * sample.Tracker written by Matthew Nelson.
 * 
 * @author George Lee
 * 
 */
public class Menehune extends Robot {
  /** Determines how much we should turn the radar per turn. */
  private static final double TURN_INCREMENT = 20.0;
  /** Distance at which we fire at the enemy. */
  public static final double MIN_DISTANCE = 200.0;
  /** Distance at which we move when we are hit too many times. */
  private static final double MOVE_DISTANCE = 100.0;
  /** Threshold for missCount. Determines if we should find another robot to track. */
  private static final int ROBOT_MISSING = 15;
  /** Max number of hits to take before moving away. */
  private static final int MAX_HITS = 3;
  /** The name of the currently scanned robot. */
  private String scannedRobotName;
  /** Counts how many times we found a robot other than the one we're tracking. */
  private int missCount;
  /** Counts how many times we have been hit. */
  private int hitCount = 0;

  /** Determines our attack/movement modes for this robot. */
  enum RobotMode {
    FIRE_MODE, TRACKING_MODE, SEARCH_MODE
  };

  /** The current mode of the robot. */
  private RobotMode mode;

  /**
   * Gets the current hit count of this robot.
   * 
   * @return The number of hits registered to this robot.
   */
  int getHitCount() {
    return this.hitCount;
  }

  /**
   * Gets the enemy that this robot is tracking.
   * 
   * @return The name of the enemy we are tracking or null if we aren't tracking anyone.
   */
  String getScannedRobotName() {
    return this.scannedRobotName;
  }
  
  /**
   * Gets the mode of this robot.
   * 
   * @return The RobotMode of the robot.
   */
  RobotMode getMode() {
    return this.mode;
  }

  /**
   * Rotates radar to find an enemy robot.
   * 
   * @see robocode.Robot#run()
   */
  @Override
  public void run() {
    this.setAdjustGunForRobotTurn(true);
    this.missCount = 0;
    this.mode = RobotMode.SEARCH_MODE;

    // Move a little to let enemies initialize their movement patterns.
    this.ahead(200);
    // Scan for a robot.
    while (true) {
      // Scan for enemy.
      if (this.mode == RobotMode.SEARCH_MODE) {
        this.turnGunLeft(TURN_INCREMENT);
      }

      this.missCount++;
      out.println("Miss count is " + this.missCount);
      // If we found many robots without finding our original robot, find a new robot to track.
      if (this.missCount > ROBOT_MISSING) {
        this.missCount = 0;
        this.scannedRobotName = null;
        this.mode = RobotMode.SEARCH_MODE;
      }
    }
  }

  /**
   * Finds a robot to track. If we're already tracking a robot, decides what to do based on the
   * enemy's velocity.
   * 
   * @param event The event that triggered this method.
   */
  @Override
  public void onScannedRobot(ScannedRobotEvent event) {
    // If we're not currently tracking a robot, track it.
    if (this.scannedRobotName == null) {
      this.scannedRobotName = event.getName();
    }

    // If this isn't the robot we're looking for, continue scanning.
    else if (!this.scannedRobotName.equals(event.getName())) {
      this.missCount++;
      return;
    }

    if (this.mode == RobotMode.SEARCH_MODE) {
      this.mode = RobotMode.TRACKING_MODE;
    }

    // We found our robot.
    this.missCount = 0;

    this.attackEnemy(event);
  }

  /**
   * Attacks a moving enemy.
   * 
   * @param event The event that contains the data about the enemy robot.
   */
  private void attackEnemy(ScannedRobotEvent event) {
    if (event.getDistance() < MIN_DISTANCE) {
      this.mode = RobotMode.FIRE_MODE;
    }
    else {
      this.mode = RobotMode.TRACKING_MODE;
    }

    // If we are in tracking mode, we want to approach the enemy to get into firing range.
    if (this.mode == RobotMode.TRACKING_MODE) {
      if (event.getBearing() < 180.0) {
        this.turnRight(event.getBearing());
      }
      else {
        this.turnLeft(event.getBearing());
      }
      this.ahead(0.5 * event.getDistance());
      this.scan();
    }
    // Else, if we are in firing mode, track enemy with gun and fire.
    else if (this.mode == RobotMode.FIRE_MODE) {
      this.fire(3);
      if (!Utils.isNear(event.getVelocity(), 0)) {
        out.println("Event velocity is " + event.getVelocity());
        this.turnGunRight(findGunTurnAngle(event));
      }

      this.scan();
    }
  }

  /**
   * Determines the angle at which we should turn the gun to track the enemy.
   * 
   * @param event The event that contains information about the enemy robot.
   * @return The turn angle for the gun to find the enemy robot.
   */
  private double findGunTurnAngle(ScannedRobotEvent event) {
    // Adapted from Firing04
    double absoluteHeading = this.getGunHeading() + event.getBearing();
    // Use the law of cosines to approximate the new distance after the enemy moves.
    double predictedAngle = 360.0 - event.getHeading() - (180 - absoluteHeading);
    double predictedDist =
        Math.pow(event.getDistance(), 2) + Math.pow(event.getVelocity(), 2)
            - (2 * event.getDistance() * event.getVelocity() * Math.cos(predictedAngle));

    // Use the law of sines to approximate the new absolute heading
    double turnAngle = Math.asin((event.getVelocity() / predictedDist) * Math.sin(predictedAngle));
    if (turnAngle > 180.0) {
      turnAngle -= 360.0;
    }

    return turnAngle;
  }

  /**
   * If we hit a robot, we need to move out of the way.
   * 
   * @param event The event that triggered the method.
   */
  @Override
  public void onHitRobot(HitRobotEvent event) {
    // If the enemy hit us, the enemy is probably not stationary as we would've moved away from it
    // earlier.
    this.mode = RobotMode.TRACKING_MODE;
    // Retaliate before moving. Borrowed from Tracker
    double gunTurn =
        normalRelativeAngleDegrees(event.getBearing()
            + (this.getHeading() - this.getRadarHeading()));
    this.turnGunRight(gunTurn);
    this.fire(3);

    this.turnRight(this.getPerpendicularTurnAngle(event.getBearing()));
    this.ahead(MOVE_DISTANCE);
  }

  /**
   * If we are hit too many times, it's time to move.
   * 
   * @param event The event that triggered this method.
   */
  @Override
  public void onHitByBullet(HitByBulletEvent event) {
    this.hitCount++;

    // Add an additional hit count cause this hurts so much.
    if (event.getPower() > 2) {
      this.hitCount++;
    }

    if (hitCount > MAX_HITS && this.mode == RobotMode.FIRE_MODE) {
      this.hitCount = 0;
      double turnAngle = this.getPerpendicularTurnAngle(event.getBearing());
      this.turnRight(this.getPerpendicularTurnAngle(turnAngle));
      if (turnAngle > 0) {
        this.turnGunLeft(30.0);
      }
      else {
        this.turnGunRight(30.0);
      }
      this.ahead(MOVE_DISTANCE);
      this.scan();
    }
  }

  /**
   * Calculate the turn angle so that we move 90 degrees from the point of impact.
   * 
   * @param eventBearing The bearing where the event occurred.
   * 
   * @return The angle to turn.
   */
  public double getPerpendicularTurnAngle(double eventBearing) {
    double turnValue = eventBearing + 90.0;
    if (turnValue < 180.0) {
      return turnValue;
    }
    return turnValue * -1.0;
  }
}
